# frozen_string_literal: true

require 'yaml'
require 'spaceship'
require 'fileutils'
require 'json'
require 'dotenv'

require_relative 'monkey_patches/supply_reader_extension'
require_relative 'monkey_patches/supply_custom_promote_config'
require_relative 'monkey_patches/supply_custom_promote'
require_relative 'monkey_patches/badge_colorspace_fix'

Dotenv.load('../.env.releases')

skip_docs

# Constants and Path Configurations
$APP_JSON_PATH = '../app.json'
$APP_JSON = JSON.parse(File.read($APP_JSON_PATH))
APP_PLIST = '../ios/Artsy/App_Resources/Info.plist'
FIREBASE_BADGE_TEXT_COLOR = '8A2BE2' # devpurple
FIREBASE_BADGE_IMAGE_PATH = 'fastlane/images/beta_badge.png'
BUILD_GRADLE = '../android/app/build.gradle'
BUNDLE_ID = 'net.artsy.artsy'

IOS_TARGETS = {
  'net.artsy.artsy' => { name: 'Artsy', plist_path: APP_PLIST },
  'net.artsy.artsy.Artsy-Stickers' => { name: 'ArtsyStickers', plist_path: '../ios/ArtsyStickers/Info.plist' },
  'net.artsy.artsy.ArtsyWidget' => { name: 'ArtsyWidgetExtension', plist_path: '../ios/ArtsyWidget/Info.plist' },
  'net.artsy.artsy.BrazePushServiceExtension' => { name: 'BrazePushServiceExtension',
                                                   plist_path: '../ios/BrazePushServiceExtension/Info.plist' }
}

IOS_BETA_TARGETS = {
  'net.artsy.qa' => { name: 'Artsy', plist_path: APP_PLIST },
  'net.artsy.qa.Artsy-Stickers' => { name: 'ArtsyStickers', plist_path: '../ios/ArtsyStickers/Info.plist' },
  'net.artsy.qa.ArtsyWidget' => { name: 'ArtsyWidgetExtension', plist_path: '../ios/ArtsyWidget/Info.plist' },
  'net.artsy.qa.BrazePushServiceExtension' => { name: 'BrazePushServiceExtension',
                                                   plist_path: '../ios/BrazePushServiceExtension/Info.plist' }
}

BETA_LANES = %i[ship_beta ship_beta_ios ship_beta_android build_maestro_android build_maestro_ios deploy_android_beta deploy_ios_beta].freeze

GIT_COMMIT_SHORT_HASH = `git log -n1 --format='%h'`.chomp
GIT_COMMIT_HASH = `git log -n1 --format='%H'`.chomp
GIT_COMMIT_DATE_STR = DateTime.parse(`git log -n1 --format='%ci'`.chomp).iso8601
GIT_REMOTE_ORIGIN_URL = `git config --get remote.origin.url`.chomp

S3_PATH = 's3://artsy-citadel/eigen'
S3_ANDROID_BUILDS_PATH = S3_PATH + '/builds/android/'
S3_IOS_BUILDS_PATH = S3_PATH + '/builds/ios/'

import 'utility_fastlane.rb'
import 'sentry_fastlane.rb'

# Circle CI Setup

before_all do
  setup_circle_ci
end

# iOS Lanes

desc 'Ships iOS beta to TestFlight or Firebase'
lane :ship_beta_ios do |options|
  deployment_target = options[:deployment_target] || 'testflight' # default to 'testflight'

  bundle_version = set_build_version_ios
  set_git_properties_ios

  latest_version = $APP_JSON['version']

  # Builds the app
  sync_signing_ios(deployment_target: deployment_target)
  tag_and_push(tag: "ios-#{latest_version}-#{bundle_version}")

  # Add badge if firebase deployment
  if deployment_target == 'firebase'
    puts "Adding badge for firebase deployment..."
    # The spaces at the beginning and end are important to avoid the badge being cut off
    add_badge(shield: " #{bundle_version}-#{FIREBASE_BADGE_TEXT_COLOR} ", custom: FIREBASE_BADGE_IMAGE_PATH)
    UI.success("✅ Badge added for firebase deployment")
  end

  # Workaround for timeout in ci
  ENV["FASTLANE_XCODEBUILD_SETTINGS_TIMEOUT"] = "120"

  # Select Xcode version
  xcode_select("/Applications/Xcode_16.4.app")

  build_params = ios_build_params(deployment_target)
  build_ios_app(**build_params)

  # avoid duplicate uploads to sentry
  if (deployment_target == 'testflight')
    # important! this must match the release version specified
    # in Eigen in order for sourcemaps to work correctly
    sentry_release_name = "ios-#{latest_version}-#{bundle_version}"
    extract_ios_bundle_and_sourcemap(
      archive_root: "../archives",
      dist_dir: "../dist/ios",
      app_name: "Artsy"
    )

    upload_sentry_artifacts(
      sentry_release_name: sentry_release_name,
      dist_version: bundle_version,
      platform: 'ios'
    )
  elsif (deployment_target == 'firebase')
    upload_dsyms_to_sentry(
      project_slug: 'eigen',
      org_slug: 'artsynet'
    )
  end

  deploy_ios_beta(deployment_target: deployment_target)

  # Save the generated version for CI/CD to use
  version_info = "#{latest_version} (#{bundle_version})"
  version_file = File.join(__dir__, 'ios_beta_version.txt')
  File.write(version_file, version_info)
  UI.success("✅ iOS beta version saved: #{version_info}")
end

desc 'Deploy iOS beta to TestFlight or Firebase'
lane :deploy_ios_beta do |options|
  deployment_target = options[:deployment_target]
  if deployment_target == 'testflight'
    api_key = generate_app_store_connect_api_key

    beta_readme = 'Thanks for using the Artsy beta!'
    # Send to the app store
    beta_app_review_info = {
      contact_email: ENV['BETA_CONTACT_EMAIL'],
      contact_first_name: ENV['BETA_CONTACT_FIRST_NAME'],
      contact_last_name: ENV['BETA_CONTACT_LAST_NAME'],
      contact_phone: ENV['BETA_CONTACT_PHONE'],
      demo_account_name: ENV['BETA_DEMO_ACCOUNT_NAME'],
      demo_account_password: ENV['BETA_DEMO_ACCOUNT_PWD']
    }

    pilot(
      api_key: api_key,
      ipa: './Artsy.ipa',
      beta_app_review_info: beta_app_review_info,
      changelog: beta_readme,
      itc_provider: 'ArtsyInc',
      distribute_external: true,
      wait_processing_timeout_duration: 900, # 15 minutes
      groups: ['Artsy People', 'Applause People']
    )
  else
    firebase_app_distribution(
      app: ENV['FIREBASE_IOS_APP_ID'],
      ipa_path:  './Artsy.ipa',
      groups: 'artsy-people',
    )
  end
end

def sync_signing_ios(deployment_target:)
  ios_targets = deployment_target == 'testflight' ? IOS_TARGETS : IOS_BETA_TARGETS
  code_signing_type = deployment_target == 'testflight' ? 'appstore' : 'adhoc'

  sync_code_signing(app_identifier: ios_targets.keys, type: code_signing_type)

  ios_targets.each do |id, meta|
    code_signing_profile = deployment_target == 'testflight' ? "match AppStore #{id}" : "match AdHoc #{id}"
    update_code_signing_settings(
      use_automatic_signing: false,
      code_sign_identity: 'Apple Distribution: Art.sy Inc. (23KMWZ572J)',
      path: 'ios/Artsy.xcodeproj',
      team_id: '23KMWZ572J',
      targets: [meta[:name]],
      profile_name: code_signing_profile
    )
  end
end

desc 'Promote a testflight iOS beta to the app store'
lane :promote_beta_ios do
  api_key = generate_app_store_connect_api_key

  # There seems to be some delta between spaceship + deliver token format
  token = generate_spaceship_token
  Spaceship::ConnectAPI.token = token

  app = Spaceship::ConnectAPI::App.find('net.artsy.artsy')
  next_app_store_version = app.get_edit_app_store_version.version_string

  # app.builds are listed most recent first, limit to 20
  builds = app.get_builds.first(20).map { |build| build.version }

  UI.header 'Last 20 iOS builds'

  build_number = UI.select('Which build would you like to release?: ', builds)

  if UI.confirm("Are you sure you would like to release '#{build_number}'?")
    UI.success 'Continuing the release!'
  else
    UI.user_error!('Stopping the train!')
  end

  UI.success("Let's deliver beta #{next_app_store_version} (#{build_number}) with build number #{build_number}.")

  deliver(
    api_key: api_key,
    app_version: $APP_JSON['version'],
    build_number: build_number,
    submit_for_review: true,
    automatic_release: true,
    phased_release: true
  )

  UI.message('Tagging submission and pushing to GitHub.')

  formatted_build_number = format_build_number(build_number)

  tag_and_push(tag: "ios-#{next_app_store_version}-#{formatted_build_number}-submission")

  sentry_slack_ios(build_number: formatted_build_number, version: next_app_store_version)

  UI.success('All done!')
end

desc 'Set the build version in the iOS app plist'
lane :set_build_version_ios do
  bundle_version = DateTime.now.strftime('%Y.%m.%d.%H')
  IOS_TARGETS.each do |_id, meta|
    absolute_path = File.expand_path(meta[:plist_path])
    set_info_plist_value(path: absolute_path, key: 'CFBundleShortVersionString', value: $APP_JSON['version'])
    set_info_plist_value(path: absolute_path, key: 'CFBundleVersion', value: bundle_version)
  end
  bundle_version
end

desc 'Set some git properties in iOS for reading in builds'
lane :set_git_properties_ios do
  absolute_path = File.expand_path(APP_PLIST)
  set_info_plist_value(path: absolute_path, key: 'GITCommitShortHash', value: GIT_COMMIT_SHORT_HASH)
  set_info_plist_value(path: absolute_path, key: 'GITCommitHash', value: GIT_COMMIT_HASH)
  set_info_plist_value(path: absolute_path, key: 'GITCommitDate', value: GIT_COMMIT_DATE_STR)
  set_info_plist_value(path: absolute_path, key: 'GITRemoteOriginURL', value: GIT_REMOTE_ORIGIN_URL)
end

desc 'Create a new version in app store connect if one does not exist'
lane :create_next_version_if_needed do
  api_token = generate_spaceship_token
  Spaceship::ConnectAPI.token = api_token

  app = Spaceship::ConnectAPI::App.find(BUNDLE_ID)

  edit_version = app.get_edit_app_store_version(platform: "iOS")
  live_version = app.get_live_app_store_version(platform: "iOS")

  edit_version_ready = edit_version && edit_version.app_store_state == 'PREPARE_FOR_SUBMISSION'
  live_version_ready = live_version && live_version.app_store_state == 'READY_FOR_SALE'
  if live_version_ready and !edit_version_ready then
    UI.message("There is a live app version but there is no editable version ready")

    non_editable_states = ['WAITING_FOR_REVIEW', 'IN_REVIEW', 'PENDING_APPLE_RELEASE', 'PROCESSING_FOR_APP_STORE']
    if edit_version && non_editable_states.include?(edit_version.app_store_state)
      UI.message("An editable version exists but is not editable (state: #{edit_version.app_store_state})")
      UI.message("We can't create a new version until the editable version is approved and ready")
      next
    end

    next_version = increment_version_number(live_version.version_string)
    UI.message("Creating new version #{next_version}")
    update_version_string(version: next_version)
    create_next_app_version(next_version_code:  next_version)
    pr_url = prepare_version_update_pr(commit_message: "chore: prepare for next release")
    message = <<~MSG
    :pr-open: :iphone:
    It's time to update the app version number!
    Merge this pr to keep shipping betas:
    #{pr_url}
  MSG
    slack(message: message, default_payloads: [], link_names: true)
  else
    if (edit_version_ready) then
      UI.success("There is an editable version ready for betas.")
    else
      message = <<~MSG
      :x: :iphone:
      New app version could not be created.
      Possible reasons: The app was rejected, or the app is not in a state to be edited.
      Check app store connect for specific details.
      Once resolved, create a new version in app store connect to keep shipping betas.
      https://appstoreconnect.apple.com/apps/703796080/appstore
    MSG
      slack(message: message, default_payloads: [], link_names: true)
    end
  end
end

# Android Lanes

desc 'Ship android beta to play store or firebase'
lane :ship_beta_android do |options|
  deployment_target = options[:deployment_target] || 'play_store' # default to 'play_store'
  version_code = options[:version_code]
  sh('yarn relay')

  if version_code.nil? || version_code.empty?
    vname, vcode = set_build_version_android
  else
    vname, vcode = set_build_version_android(version_code: version_code)
  end

  set_git_properties_android
  tag_and_push(tag: "android-#{vname}-#{vcode}")

  build_type = deployment_target == 'play_store' ? 'release' : 'beta'
  sh("yarn react-native build-android --mode=#{build_type} --extra-params='--no-daemon --max-workers 2'")

  if (deployment_target == 'play_store')
    # important! this must match the release version specified
    # in Eigen in order for sourcemaps to work correctly
    sentry_release_name = "android-#{vname}-#{vcode}"
    upload_sentry_artifacts(
      sentry_release_name: sentry_release_name,
      dist_version: "#{vcode}",
      platform: 'android'
    )
    s3_upload_android_build(app_version: vcode, app_path: "../android/app/build/outputs/bundle/release/app-release.aab")
  end

  deploy_android_beta(deployment_target: deployment_target)

  # Save the generated version for CI/CD to use
  version_info = "#{vname} (#{vcode})"
  version_file = File.join(__dir__, 'android_beta_version.txt')
  File.write(version_file, version_info)
  UI.success("✅ Android beta version saved: #{version_info}")
end

desc 'Deploy Android beta to Play Store or Firebase'
lane :deploy_android_beta do |options|
  deployment_target = options[:deployment_target]
  if deployment_target == 'play_store'
    supply(
      track: 'alpha',
      skip_upload_apk: true,
      skip_upload_metadata: true,
      skip_upload_changelogs: true,
      skip_upload_images: true,
      skip_upload_screenshots: true,
      aab: './android/app/build/outputs/bundle/release/app-release.aab'
    )
  elsif deployment_target == 'firebase'
    firebase_app_distribution(
      app: ENV['FIREBASE_ANDROID_APP_ID'],
      groups: 'artsy-people',
      android_artifact_path: './android/app/build/outputs/bundle/beta/app-beta.aab'
    )
  else
    UI.error("Unknown deployment target: #{deployment_target}")
  end
end

desc 'Promote a play store android beta to production'
lane :promote_beta_android do
  selected_version_code = select_android_build(skip_download: true)

  supply(
    track: 'alpha',
    skip_release_verification: true,
    version_code: selected_version_code,
    track_promote_to: 'production',
    rollout: '0.1',
    skip_upload_images: true,
    skip_upload_screenshots: true,
    skip_upload_aab: true,
    skip_upload_apk: true
  )

  vname, vcode = set_build_version_android(version_code: selected_version_code)
  tag_and_push(tag: "android-#{vname}-#{vcode}-submission")
  sentry_slack_android(build_number: vcode, version: vname)
end

lane :create_android_apk do
  selected_version_code = select_android_build(select_message: 'select')
  aab_path = './android/app/build/outputs/bundle/release/app-release.aab'
  bundletool(
    aab_path: aab_path,
    apk_output_path: selected_version_code + '.apk',
    verbose: true
  )
end

lane :select_android_build do |options|
  select_message = options[:select_message] || 'release'
  skip_download = options[:skip_download] || false

  s3_files_output = sh('aws s3 ls ' + S3_ANDROID_BUILDS_PATH)

  aab_filename_regex = /\d+\.aab/
  matches = s3_files_output.scan(aab_filename_regex)

  # sort in descending order
  sorted_matches = matches.sort_by { |s| s.scan(/\d+/).first.to_i }.reverse

  # android builds are listed most recent first, limit to 20
  builds = sorted_matches.first(20)

  UI.header 'Last 20 Android builds'

  selected_build = UI.select('Which build would you like to ' + select_message + '?: ', builds)
  if UI.confirm("Are you sure you would like to " + select_message + " #{selected_build}?")
    UI.success 'Continuing!'
  else
    UI.user_error!('Stopping the train!')
  end

  if !skip_download
    # download the selected build to the typical build output directory
    output_app_path = '../android/app/build/outputs/bundle/release/app-release.aab'
    sh('aws s3 cp ' + S3_ANDROID_BUILDS_PATH + selected_build + ' ' + output_app_path)
  end

  # return the selected version num
  selected_build.scan(/\d+/).first
end

desc 'Set the build version in the android app build.gradle'
lane :set_build_version_android do |options|
  version_code = options[:version_code]

  next_version_name = $APP_JSON['version']

  if version_code.nil?
    current_version_code = google_play_track_version_codes(
      track: 'alpha'
    ).first
    version_code = current_version_code + 1
  end

  next_version_code = version_code

  contents = File.read(BUILD_GRADLE)

  contents = contents.gsub(/(versionName) .*/, "\\1 \"#{next_version_name}\"")
  contents = contents.gsub(/(versionCode) (.*)/, "\\1 #{next_version_code}")

  write_contents_to_file(BUILD_GRADLE, contents)

  [next_version_name, next_version_code]
end


desc 'Get next android build version code'
lane :get_next_android_build_version do
  # Generate date-based build number in format YYYYMMDDHH (e.g., 2025111310)
  # This matches iOS format but without dots since Google Play Console doesn't accept them
  now = Time.now.utc
  next_version_code = now.strftime('%Y%m%d%H')

  # Check if this version code already exists in the alpha track
  current_version_code = google_play_track_version_codes(
    track: 'alpha'
  ).first

  # If the generated version code matches the current version code (same hour),
  # append minutes to create a unique version (YYYYMMDDHHMI format)
  if current_version_code && next_version_code.to_i == current_version_code.to_i
    next_version_code = now.strftime('%Y%m%d%H%M')
    puts "Version code conflict detected. Using extended format with minutes: #{next_version_code}"
  end

  file_dir = "#{Dir.pwd}/next_version_code.txt"
  puts "Writing next version code to: #{file_dir}"
  puts "Generated date-based version code: #{next_version_code}"
  File.write(file_dir, next_version_code)
end

desc 'Set some git properties on android for reading in builds'
lane :set_git_properties_android do
  contents = File.read(BUILD_GRADLE)

  contents = contents.gsub(/(GITCommitShortHash.* '").*("')/, "\\1#{GIT_COMMIT_SHORT_HASH}\\2")
  contents = contents.gsub(/(GITCommitHash.* '").*("')/, "\\1#{GIT_COMMIT_HASH}\\2")
  contents = contents.gsub(/(GITCommitDate.* '").*("')/, "\\1#{GIT_COMMIT_DATE_STR}\\2")
  contents = contents.gsub(/(GITRemoteOriginURL.* '").*("')/, "\\1#{GIT_REMOTE_ORIGIN_URL}\\2")

  write_contents_to_file(BUILD_GRADLE, contents)
end

desc 'Update the rollout percentage for the latest release in the play store'
lane :update_rollout_if_necessary do
  rollout_strategy = [0.1, 0.2, 0.5, 1.0]
  rollout_percentages = google_play_track_rollout_percentages(track: 'production')
  latest_release = rollout_percentages.first

  if latest_release && latest_release[:user_fraction]
    current_fraction = latest_release[:user_fraction]
    next_index = rollout_strategy.find_index(current_fraction) + 1

    if next_index && next_index < rollout_strategy.length
      next_fraction = rollout_strategy[next_index]
      supply(
        track: 'production',
        rollout: next_fraction.to_s,
        skip_upload_apk: true,
        skip_upload_metadata: true,
        skip_upload_changelogs: true,
        skip_upload_images: true,
        skip_upload_screenshots: true,
        skip_upload_aab: true
      )
      message = <<~MSG
      :android-2: :chart_with_upwards_trend:
      Android rollout updated!
      Updated from #{current_fraction * 100}% to #{next_fraction * 100}%.
    MSG
      slack(message: message, default_payloads: [], link_names: true)
    else
      UI.message("Current rollout is already at the maximum configured percentage or not part of the strategy.")
    end
  else
    UI.message("No active release found or release does not have a rollout fraction.")
  end
end

# Expo updates

lane :deploy_to_expo_updates do |options|
  deployment_name = options[:deployment_name]
  description = options[:description]
  rollout_percentage = options[:rollout_percentage]
  platform = options[:platform]

  # important! this must match the release version specified
  # in Eigen in order for sourcemaps to work correctly
  # TODO: Not sure if this is still needed or a variant needed
  # Will need to modify eigen code in any case
  release_name_base, dist_version, release_version = set_expo_release_values(deployment_name)

  expo_command = "./bin/node_modules/.bin/eas update --channel #{deployment_name} --message '#{description}' --platform #{platform}"

  if rollout_percentage
    expo_command += " --rollout-percentage=#{rollout_percentage}"
  end

  sh("pushd ../ > /dev/null && \
      set -o pipefail && \
      #{expo_command} | tail -n1 && \
      popd > /dev/null")

  # Export environment variables for Sentry sourcemap upload
  ENV['SENTRY_RELEASE'] = "#{platform}-#{release_name_base}"

  puts "🔧 Uploading sourcemaps to Sentry with release: #{platform}-#{release_name_base}"
  sh("cd .. && npx sentry-expo-upload-sourcemaps dist")

  tag_and_push(tag: "#{platform}-#{release_name_base}")

  puts "🚀 Deploying to #{deployment_name} with description '#{description}' for platform #{platform}"
  puts "🎯 Rollout: #{rollout_percentage}%" if rollout_percentage
end

def set_expo_release_values(deployment_name)
  date_str = DateTime.now.strftime('%Y.%m.%d.%H')
  latest_version = $APP_JSON['version']
  release_name_base = "expo-#{deployment_name.downcase}-#{latest_version}-#{date_str}"
  $APP_JSON['expoReleaseNameBase'] = release_name_base
  $APP_JSON['expoDist'] = date_str
  write_contents_to_file($APP_JSON_PATH, JSON.pretty_generate($APP_JSON))
  [release_name_base, date_str, latest_version]
end

error do |lane, exception|
  return unless BETA_LANES.include?(lane)

  if should_silence_beta_failure?
    puts('Ignoring beta failure, make sure to unset FASTLANE_SILENCE_BETA_FAILURES_UNTIL to receive alerts')
    return
  end

  if is_ci
    notify_beta_failed(exception: exception)
  else
    UI.error("Beta failed to deploy! #{exception.message}")
  end
end

desc 'Build iOS app and upload to S3 for Maestro testing'
lane :build_maestro_ios do
  bundle_version = set_build_version_ios
  set_git_properties_ios

  # Select Xcode version
  xcode_select("/Applications/Xcode_16.4.app")

  # Build iOS app with QA scheme for simulator (no codesigning needed)
  build_params = ios_build_params('maestro')
  build_ios_app(**build_params)

  upload_ios_maestro_to_s3
end

desc 'Build Android app and upload to S3 for Maestro testing'
lane :build_maestro_android do |options|
  version_code = options[:version_code]
  sh('yarn relay')

  if version_code.nil? || version_code.empty?
    vname, vcode = set_build_version_android
  else
    vname, vcode = set_build_version_android(version_code: version_code)
  end

  set_git_properties_android

  # Build Android AAB in beta mode for Maestro testing
  sh("yarn react-native build-android --mode=beta --extra-params='--no-daemon --max-workers 2'")

  # Convert AAB to APK using existing bundletool logic
  aab_path = './android/app/build/outputs/bundle/beta/app-beta.aab'
  apk_output_path = "#{vcode}.apk"
  bundletool(
    aab_path: aab_path,
    apk_output_path: apk_output_path,
    verbose: true
  )

  # Ensure we have the full path to the APK file (bundletool creates it in project root, not fastlane dir)
  full_apk_path = File.expand_path("../#{apk_output_path}")

  # Upload to S3 for Maestro testing
  upload_android_maestro_to_s3(full_apk_path)
end
